package demoMod.stsconsole.dungeons;

import demoMod.stsconsole.characters.AbstractPlayer;
import demoMod.stsconsole.chests.AbstractChest;
import demoMod.stsconsole.chests.LargeChest;
import demoMod.stsconsole.chests.MediumChest;
import demoMod.stsconsole.chests.SmallChest;
import demoMod.stsconsole.enums.MapRoomType;
import demoMod.stsconsole.events.AbstractEvent;
import demoMod.stsconsole.helpers.MapRoomChance;
import demoMod.stsconsole.helpers.PromptHelper;
import demoMod.stsconsole.helpers.RandomSequence;
import demoMod.stsconsole.helpers.RelicLibrary;
import demoMod.stsconsole.interfaces.CustomSavable;
import demoMod.stsconsole.map.DungeonMap;
import demoMod.stsconsole.map.MapNode;
import demoMod.stsconsole.relics.AbstractRelic;
import demoMod.stsconsole.rooms.*;

import java.util.*;

public abstract class AbstractDungeon implements CustomSavable<Map<String, Object>> {
    public static AbstractPlayer player;
    public static AbstractDungeon currDungeon;
    public static List<AbstractRelic> relicPool = new ArrayList<>();
    public static int floorNum;
    public static long seed;
    public String dungeonId;
    public DungeonMap map;
    public MapNode currMapNode;
    public int playerMapPosX = 3;
    public int playerMapPosY = -1;
    public List<String> weakEnemyPool;
    public List<String> strongEnemyPool;
    public List<String> eliteEnemyPool;
    public List<String> bossPool;
    public List<Class<? extends AbstractEvent>> eventPool;
    private int eventPoolIndex = 0;
    public static String currEventId = "";
    public String lastWeakEnemyEncounter = "";
    public String lastStrongEnemyEncounter = "";
    public String lastEliteEnemyEncounter = "";
    public int weakEnemyCtr = 0;
    private String bossKey = null;
    public static RandomSequence relicRng;
    public static RandomSequence cardRewardRng;
    public static RandomSequence cardRng;
    public static RandomSequence monsterRng;
    public static RandomSequence aiRng;
    public static RandomSequence potionRng;
    public static RandomSequence eventRng;
    public static RandomSequence mapRng;
    public static RandomSequence shopRng;

    public static int cardRarityBlizzardCount = 0;
    public static int eventMonsterBlizzardCount = 10;
    public static int eventChestBlizzardCount = 0;
    public static int eventShopBlizzardCount = 0;

    public List<String> passedDungeons = new ArrayList<>();

    public static Map<String, Class<? extends AbstractDungeon>> dungeons = new HashMap<>();

    public static void initialize() {
        dungeons.put(Exordium.ID, Exordium.class);
    }

    public static void initializeRng(long seed) {
        relicRng = new RandomSequence(seed);
        cardRewardRng = new RandomSequence(seed);
        cardRng = new RandomSequence(seed);
        monsterRng = new RandomSequence(seed);
        aiRng = new RandomSequence(seed);
        potionRng = new RandomSequence(seed);
        eventRng = new RandomSequence(seed);
        mapRng = new RandomSequence(seed);
        shopRng = new RandomSequence(seed);
    }

    public static void initializeRelicPool() {
        List<AbstractRelic> relics = RelicLibrary.getCombatRelics();
        relicPool.clear();
        for (AbstractRelic relic : relics) {
            relicPool.add(relic.makeCopy());
        }
    }

    public static AbstractDungeon getDungeon(String id) {
        try {
            return dungeons.get(id).newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return new Exordium();
    }

    public static AbstractDungeon getDungeon(float actNum) {
        try {
            List<AbstractDungeon> availableDungeons = new ArrayList<>();
            for (Map.Entry<String, Class<? extends AbstractDungeon>> e : dungeons.entrySet()) {
                AbstractDungeon dungeon = e.getValue().newInstance();
                if (Math.abs(dungeon.getActNum() - actNum) <= 0.0001F) {
                    availableDungeons.add(dungeon);
                }
            }
            if (availableDungeons.size() > 0) {
                Collections.shuffle(availableDungeons, mapRng);
                return availableDungeons.get(0);
            }
        } catch (InstantiationException | IllegalAccessException ex) {
            ex.printStackTrace();
        }
        return new Exordium();
    }

    public abstract float getActNum();

    @Override
    public Map<String, Object> onSave() {
        Map<String, Object> ret = new HashMap<>();
        ret.put("id", dungeonId);
        ret.put("floorNum", floorNum);
        ret.put("playerMapPosX", playerMapPosX);
        ret.put("playerMapPosY", playerMapPosY);
        ret.put("seed", seed);
        ret.put("passedDungeons", passedDungeons);
        ret.put("currRoomPhase", currMapNode.room.phase);
        ret.put("shouldDropReward", currMapNode.room.shouldDropReward);
        ret.put("currRoomGoldAmount", currMapNode.room.goldAmount);
        ret.put("cardRarityBlizzardCount", cardRarityBlizzardCount);
        ret.put("eventMonsterBlizzardCount", eventMonsterBlizzardCount);
        ret.put("eventChestBlizzardCount", eventChestBlizzardCount);
        ret.put("eventShopBlizzardCount", eventShopBlizzardCount);
        ret.put("currEventId", currEventId);
        ret.put("weakEnemyCtr", this.weakEnemyCtr);
        ret.put("lastWeakEnemyEncounter", this.lastWeakEnemyEncounter);
        ret.put("lastStrongEnemyEncounter", this.lastStrongEnemyEncounter);
        ret.put("lastEliteEnemyEncounter", this.lastEliteEnemyEncounter);
        ret.put("eventPoolIndex", this.eventPoolIndex);
        List<Long> rngCounter = new ArrayList<>();
        rngCounter.add(relicRng.counter);
        rngCounter.add(cardRewardRng.counter);
        rngCounter.add(cardRng.counter);
        rngCounter.add(monsterRng.counter);
        rngCounter.add(aiRng.counter);
        rngCounter.add(potionRng.counter);
        rngCounter.add(eventRng.counter);
        rngCounter.add(shopRng.counter);
        ret.put("rngCounters", rngCounter);
        List<String> relicPool = new ArrayList<>();
        for (AbstractRelic relic : AbstractDungeon.relicPool) {
            relicPool.add(relic.id);
        }
        ret.put("relicPool", relicPool);
        return ret;
    }

    @Override
    public void onLoad(Map<String, Object> data) {
        if (data != null) {
            AbstractDungeon.floorNum = (int) data.get("floorNum");
            this.playerMapPosX = (int) data.get("playerMapPosX");
            this.playerMapPosY = (int) data.get("playerMapPosY");
            this.passedDungeons.addAll((List<String>) data.get("passedDungeons"));
            cardRarityBlizzardCount = (int) data.get("cardRarityBlizzardCount");
            eventMonsterBlizzardCount = (int) data.get("eventMonsterBlizzardCount");
            eventChestBlizzardCount = (int) data.get("eventChestBlizzardCount");
            eventShopBlizzardCount = (int) data.get("eventShopBlizzardCount");
            currEventId = (String) data.get("currEventId");
            this.weakEnemyCtr = (int) data.get("weakEnemyCtr");
            this.lastWeakEnemyEncounter = (String) data.get("lastWeakEnemyEncounter");
            this.lastStrongEnemyEncounter = (String) data.get("lastStrongEnemyEncounter");
            this.lastEliteEnemyEncounter = (String) data.get("lastEliteEnemyEncounter");
            this.eventPoolIndex = (int) data.get("eventPoolIndex");
            List<Number> rngCounter = (List<Number>) data.get("rngCounters");
            relicRng.counter = rngCounter.get(0).longValue();
            cardRewardRng.counter = rngCounter.get(1).longValue();
            cardRng.counter = rngCounter.get(2).longValue();
            monsterRng.counter = rngCounter.get(3).longValue();
            aiRng.counter = rngCounter.get(4).longValue();
            potionRng.counter = rngCounter.get(5).longValue();
            eventRng.counter = rngCounter.get(6).longValue();
            shopRng.counter = rngCounter.get(7).longValue();
            for (String dungeonId : passedDungeons) {
                AbstractDungeon dungeon = getDungeon(dungeonId);
                dungeon.generateMap();
            }

            relicRng.resume();
            cardRewardRng.resume();
            cardRng.resume();
            monsterRng.resume();
            aiRng.resume();
            potionRng.resume();
            eventRng.resume();
            shopRng.resume();

            List<String> relicPool = (List<String>) data.get("relicPool");
            for (AbstractRelic relic : AbstractDungeon.relicPool) {
                relic.onRemove();
            }
            AbstractDungeon.relicPool.clear();
            for (String relicId : relicPool) {
                AbstractDungeon.relicPool.add(RelicLibrary.getRelic(relicId).makeCopy());
            }
        } else {
            AbstractDungeon.floorNum = 0;
            AbstractDungeon.initializeRelicPool();
        }
        this.bossPool = getBossPool();
        this.eventPool = getEventPool();
        Collections.shuffle(this.eventPool, eventRng);
        this.map = generateMap();
        if (playerMapPosX == 3 && playerMapPosY == -1) {
            List<List<MapNode>> nodes = map.getNodes();
            this.currMapNode = new MapNode();
            for (MapNode node : nodes.get(0)) {
                if (node.next.size() > 0) {
                    this.currMapNode.next.add(node);
                    node.prev.add(this.currMapNode);
                }
            }
        } else {
            this.currMapNode = map.getNode(playerMapPosX, playerMapPosY);
            if (data != null) {
                this.currMapNode.room.phase = AbstractRoom.RoomPhase.valueOf((String) data.getOrDefault("currRoomPhase", AbstractRoom.RoomPhase.COMPLETED.name()));
                this.currMapNode.room.shouldDropReward = (boolean) data.get("shouldDropReward");
                this.currMapNode.room.goldAmount = (int) data.get("currRoomGoldAmount");
            }
        }
        this.weakEnemyPool = getWeakEnemyPool();
        this.strongEnemyPool = getStrongEnemyPool();
        this.eliteEnemyPool = getEliteEnemyPool();
        this.currMapNode.room.onPlayerEntry();
    }

    public DungeonMap generateMap() {
        DungeonMap map = new DungeonMap(7, 15, 6, mapRng);
        getBoss();
        List<List<MapNode>> nodes = map.getNodes();
        for (MapNode node : nodes.get(nodes.size() - 1)) {
            if (node.prev.size() > 0) {
                node.room = new RestRoom();
            }
        }
        for (MapNode node : nodes.get(0)) {
            if (node.next.size() > 0) {
                node.room = new MonsterRoom();
            }
        }
        for (MapNode node : nodes.get(8)) {
            if (node.next.size() > 0) {
                node.room = new TreasureRoom();
            }
        }
        MapRoomChance chance = getRoomTypeChance();
        int roomCount = 0;
        List<MapNode> nodesNeedsToAssign = new ArrayList<>();
        for (List<MapNode> mapNodes : nodes) {
            for (MapNode node : mapNodes) {
                if (node.hasEdges() && node.room instanceof EmptyRoom) {
                    roomCount++;
                    nodesNeedsToAssign.add(node);
                }
            }
        }
        int eliteRoomCount = (int) (roomCount * (chance.eliteRoomChance / 100.0));
        int eventRoomCount = (int) (roomCount * (chance.eventRoomChance / 100.0));
        int restRoomCount = (int) (roomCount * (chance.restRoomChance / 100.0));
        int shopRoomCount = (int) (roomCount * (chance.shopRoomChance / 100.0));
        int monsterRoomCount = roomCount - (eliteRoomCount + eventRoomCount + restRoomCount + shopRoomCount);
        List<MapRoomType> roomTypePool = new ArrayList<>();
        while (eliteRoomCount > 0) {
            roomTypePool.add(MapRoomType.ELITE);
            eliteRoomCount--;
        }
        while (eventRoomCount > 0) {
            roomTypePool.add(MapRoomType.EVENT);
            eventRoomCount--;
        }
        while (restRoomCount > 0) {
            roomTypePool.add(MapRoomType.REST);
            restRoomCount--;
        }
        while (shopRoomCount > 0) {
            roomTypePool.add(MapRoomType.SHOP);
            shopRoomCount--;
        }
        while (monsterRoomCount > 0) {
            roomTypePool.add(MapRoomType.MONSTER);
            monsterRoomCount--;
        }
        for (MapNode node : nodesNeedsToAssign) {
            if (!roomTypePool.isEmpty()) {
                MapRoomType type = roomTypePool.remove(mapRng.nextInt(roomTypePool.size()));
                while (!type.canSetToThisType(node)) {
                    boolean shouldBreak = true;
                    for (MapRoomType type1 : roomTypePool) {
                        if (type1.canSetToThisType(node)) {
                            shouldBreak = false;
                            break;
                        }
                    }
                    if (shouldBreak) {
                        type = MapRoomType.MONSTER;
                        break;
                    }
                    roomTypePool.add(type);
                    type = roomTypePool.remove(mapRng.nextInt(roomTypePool.size()));
                }
                node.room = type.getRoom();
            } else {
                node.room = new MonsterRoom();
            }
        }
        return map;
    }

    protected MapRoomChance getRoomTypeChance() {
        MapRoomChance chance = new MapRoomChance();
        chance.eliteRoomChance = 12;
        chance.eventRoomChance = 33;
        chance.restRoomChance = 12;
        chance.shopRoomChance = 6;
        return chance;
    }

    public MapNode getSpecialMapNode(int y) {
        int mapHeight = this.map.getNodes().size();
        MapNode node = new MapNode();
        if (y == mapHeight) {
            node.y = mapHeight;
            node.room = new MonsterRoomBoss();
        } else if (y == mapHeight + 1) {
            node.y = mapHeight + 1;
            node.room = new TreasureRoom();
        } else {
            node.y = mapHeight + 2;
            node.room = new DungeonTransitionRoom();
        }
        return node;
    }

    protected abstract List<String> getWeakEnemyPool();

    protected abstract List<String> getStrongEnemyPool();

    protected abstract List<String> getEliteEnemyPool();

    protected abstract List<String> getBossPool();

    protected abstract List<Class<? extends AbstractEvent>> getEventPool();

    public AbstractEvent getEvent() {
        try {
            if (eventPoolIndex >= eventPool.size()) {
                eventPoolIndex = 0;
                Collections.shuffle(eventPool, eventRng);
            }
            return eventPool.get(eventPoolIndex).newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    public AbstractEvent getEventInCurrDungeon(String id) {
        try {
            for (Class<? extends AbstractEvent> eventCls : eventPool) {
                AbstractEvent event = eventCls.newInstance();
                if (event.id.equals(id)) {
                    return event;
                }
                PromptHelper.backwardPrompt();
            }
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void increaseEventPoolIndex() {
        eventPoolIndex++;
    }

    public String getEncounter(AbstractRoom currRoom) {
        String encounterId;
        if (!currRoom.getRoomSymbol().equals("E")) {
            if ("B".equals(currRoom.getRoomSymbol())) {
                encounterId = getBoss();
                consumeBossKey();
            } else {
                if (weakEnemyCtr < 2) {
                    encounterId = this.weakEnemyPool.get(monsterRng.nextInt(this.weakEnemyPool.size()));
                    String[] tmp = encounterId.split("_");
                    while (tmp[0].equals(this.lastWeakEnemyEncounter.split("_")[0])) {
                        encounterId = this.weakEnemyPool.get(monsterRng.nextInt(this.weakEnemyPool.size()));
                    }
                } else {
                    encounterId = this.strongEnemyPool.get(monsterRng.nextInt(this.strongEnemyPool.size()));
                    String[] tmp = encounterId.split("_");
                    while (tmp[0].equals(this.lastStrongEnemyEncounter.split("_")[0])) {
                        encounterId = this.strongEnemyPool.get(monsterRng.nextInt(this.strongEnemyPool.size()));
                    }
                }
            }
        } else {
            encounterId = this.eliteEnemyPool.get(monsterRng.nextInt(this.eliteEnemyPool.size()));
            String[] tmp = encounterId.split("_");
            while (tmp[0].equals(this.lastEliteEnemyEncounter.split("_")[0])) {
                encounterId = this.eliteEnemyPool.get(monsterRng.nextInt(this.eliteEnemyPool.size()));
            }
        }
        return encounterId;
    }

    public String getBoss() {
        if (bossKey == null) {
            if (bossPool.isEmpty()) {
                bossKey = "ApologySlime";
            } else {
                bossKey = bossPool.remove(mapRng.nextInt(bossPool.size()));
            }
        }
        return bossKey;
    }

    public void consumeBossKey() {
        bossKey = null;
    }

    public static AbstractChest getRandomChest() {
        int roll = cardRewardRng.nextInt(100);
        if (roll < 50) {
            return new SmallChest();
        } else if (roll < 75) {
            return new MediumChest();
        } else {
            return new LargeChest();
        }
    }
}
